package opengraph.impl

import opengraph.api.*
import java.io.ByteArrayInputStream
import java.io.InputStream
import java.net.HttpURLConnection
import java.net.URI
import java.nio.charset.Charset
import java.util.regex.Pattern

open class HtmlFetcherImpl : HtmlFetcher {
    // 查找 <meta charset="...">
    private val charsetPattern = "<meta[^>]+charset *= *['\"]*([\\w-]+)['\"]*".toRegex(RegexOption.IGNORE_CASE)

    // 查找 <meta http-equiv="Content-Type" ...>
    private val contentTypePattern =
        "<meta[^>]+http-equiv *= *['\"]*Content-Type['\"][^>]*content *= *['\"]*text/html;\\s*charset *= *([\\w-]+)['\"]*".toRegex(
            RegexOption.IGNORE_CASE
        )


    override fun fetch(url: String): String {
        val ins = openUrl(url) ?: return ""
        var headHtml = ""
        try {
            //是否需要回溯
            var reset = false
            //回溯的编码类型
            var charset: Charset? = null
            //先缓存流
            val byteArray = ins.readBytes()
            var bis = ByteArrayInputStream(byteArray)
            //先用utf-8读取一次
            headHtml = buildString {
                bis.bufferedReader(Charsets.UTF_8).useLines {
                    for (line in it) {
                        append(line)
                        //如果是utf-8就不要重复解析了
                        if (charset != Charsets.UTF_8) {
                            charset = extractCharsetFromMetaTag(line)
                        }
                        //不是utf-8需要回溯重新读取
                        if (charset != null && charset != Charsets.UTF_8) {
                            reset = true
                            clear()
                            break
                        }
                        if (line.contains("</head>")) {
                            break
                        }
                    }
                }
                if (reset) {
                    bis = ByteArrayInputStream(byteArray)
                    //用新的编码读取
                    bis.bufferedReader(charset!!).useLines {
                        for (line in it) {
                            append(line)
                            if (line.contains("</head>")) {
                                break
                            }
                        }
                    }
                }
            }

        } catch (e: Exception) {
            e.printStackTrace()
        }
        return headHtml
    }

    private fun openUrl(urlString: String, tryCount: Int = 1): InputStream? {
        val uri = URI(urlString)
        val url = uri.toURL()
        val conn = url.openConnection() as HttpURLConnection
        conn.readTimeout = 10000
        conn.connectTimeout = 15000
        conn.requestMethod = "GET"
        conn.setRequestProperty("User-Agent", getUserAgent())
        conn.setRequestProperty("Content-Type", "text/html;charset=utf-8")
        conn.instanceFollowRedirects = true
        conn.doInput = true
        conn.connect()
        val responseCode = (conn.responseCode / 100) * 100
        if (responseCode == HttpURLConnection.HTTP_OK) {
            return conn.inputStream
        } else if (responseCode == HttpURLConnection.HTTP_MULT_CHOICE) {
            //最大递归重定向3次
            if (tryCount == 3) {
                return null
            }
            //重定向的情况
            val redirectUrl = conn.getHeaderField("Location")
            if (!redirectUrl.isNullOrEmpty()) {
                return openUrl(redirectUrl, tryCount + 1)
            }
        }
        return null
    }

    private fun extractCharsetFromMetaTag(line: String?): Charset? {
        line ?: return null

        runCatching {
            charsetPattern.find(line)?.groupValues?.get(1)?.let {
                return Charset.forName(it)
            }
        }

        runCatching {
            contentTypePattern.find(line)?.groupValues?.get(1)?.let {
                return Charset.forName(it)
            }
        }



        return null
    }

    protected fun getUserAgent(): String {
        return "Mozilla/5.0 (Linux; U; Android) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1"
    }

}

class HtmlParserImpl : HtmlParser {
    private val metaPattern = "<meta[^>]*property=['\"](.*?)['\"][^>]*content=['\"](.*?)['\"]".toRegex(RegexOption.IGNORE_CASE)
    private val metaPattern2 = "<meta[^>]*name=['\"](.*?)['\"][^>]*content=['\"](.*?)['\"]".toRegex(RegexOption.IGNORE_CASE)

    private val titlePattern = "<title>(.*?)</title>".toRegex(RegexOption.IGNORE_CASE)
    private val iconPattern = "<link[^>]*rel=\"(?:icon|shortcut icon|shotcut icon|apple-touch-icon)\"[^>]*href=\"(.*?)\"".toRegex(RegexOption.IGNORE_CASE)

    private val nsPattern = """<html.*\n*prefix\n*=\n*["']\n*(.*)\n*["']\n*>""".toRegex(setOf(RegexOption.IGNORE_CASE, RegexOption.DOT_MATCHES_ALL))


    override fun parse(html: String): HtmlContext {
        val namespaceContext = NamespaceContextImpl()
        if (html.isEmpty()) {
            return HtmlContext(emptyList(), namespaceContext)
        }
        val metas = buildList {
            addAll(parsePropertyMeta(html))
            addAll(parseNameMeta(html))
            parseIconMeta(html)?.let { add(it) }
            parseTitleMeta(html)?.let { add(it) }
        }
        parseNamespace(html, namespaceContext)
        return HtmlContext(metas, namespaceContext)
    }

    private fun parsePropertyMeta(html: String):List<HtmlMeta>{
        return metaPattern.findAll(html)
            .map { meta ->
                val (property, value) = meta.destructured
                return@map HtmlMeta(property, value, HtmlMeta.TYPE_OG)
            }
            .toList()
    }

    private fun parseNameMeta(html: String):List<HtmlMeta>{
        return metaPattern2.findAll(html)
            .map { meta ->
                val (name, value) = meta.destructured
                return@map HtmlMeta(name, value, HtmlMeta.TYPE_OG)
            }
            .toList()
    }

    private fun parseTitleMeta(html: String):HtmlMeta?{
        return titlePattern.find(html)?.groupValues?.get(1)?.let { title ->
            return@let HtmlMeta("title", title, HtmlMeta.TYPE_DEFAULT)
        }
    }

    private fun parseIconMeta(html: String):HtmlMeta?{
        return iconPattern.find(html)?.groupValues?.get(1)?.let { title ->
            return@let HtmlMeta("icon", title, HtmlMeta.TYPE_DEFAULT)
        }
    }

    private fun parseNamespace(html: String, namespaceContext: NamespaceContext){
        nsPattern.find(html)?.groupValues?.get(1)?.let { ns ->
            val pattern = Pattern.compile("""(\w+):\s*(\S+)""")
            val matcher = pattern.matcher(ns)

            while (matcher.find()) {
                val key = matcher.group(1)
                val value = matcher.group(2)
                if (!key.isNullOrEmpty() && !value.isNullOrEmpty()) {
                    namespaceContext.setNamespace(key, value)
                }
            }
        }
    }
}

open class NamespaceContextImpl : NamespaceContext{
    private val map = mutableMapOf<String,String>()
    override fun getNamespace(schema: String): List<String>? {
        return map.entries.filter { it.value == schema }.map { it.key }.toList()
    }

    override fun getSchema(ns: String): String? {
        return map[ns]
    }

    override fun setNamespace(ns: String, schema: String) {
        map[ns] = schema
    }

    override fun toString(): String {
        return map.toString()
    }
}

